import {CONSTANTS} from '../common/consts.js';
import {Tool} from '../common/tool.js';
import {Terminals} from '../lexer/terminal.js';
import {NonTerminals} from '../lexer/nonTerminal.js';
import {grammar} from './grammar.js';

/**
 * manage all outlined grammars within a hierarchical structure 
 * where one grammar set contains multiple grammars
 * and each grammar consists of numerous production rules
 * huizi 2024
 */
const Grammars = (()=>{
    const grammars = {};

    /**
     * create a new grammar by name
     */    
    function createGrammar(name){
        const nonTerminal = NonTerminals.valueOf(name);
        return (()=>{
            if(grammars[name]===undefined) {
                grammars[name] = grammar(name);
            }
            return grammars[name];
        })();
    }

    /**
     * a outline program has only one start node
     */
    function verifyStart(){
        const starts = grammars.filter(g=>g.type().isStart()===true);
        if(starts.length==0)  Tool.grammarErr(`one start non terminal is needed`);
        if(starts.length>1) Tool.grammarErr(`only one start non terminal is allowed`);
    }

    /**
     * calculate the 'first' or 'follow' sets iteratively, continuing the loops until no further changes occur in the sets
     */
    function compute(f){
        let productions = [];
        grammars.forEach(g=>productions = productions.concat(g.productions()));
        const changed = ()=>{
            let _changed = true;
            return {
                set: v=>_changed = v,
                get: ()=>_changed
            };
        };
        while(changed.get()===true){
            changed.set(false);
            productions.forEach(production=>{
                f(production,changed);
            })
        }
    }

    function computeFirst(){
        const traced = [];
        compute((production,changed)=>{
            let broken = false;
            const oldSize = production.first().length;
            for(let symbol in production.symbols()){
                //if travese to a terminal, then the terminal should be the first of the production, the travese can break
                if(symbol.isTerminal()){
                    production.first().push(symbol);
                    broken = true;
                    break;
                }
                //if non terminal
                const grammar = grammars[symbol.name()];
                if(grammar === undefined){
                    Tool.grammarErr(`symbol: ${symbol.name()} is not found in all possible grammars`);
                }
                //the grammar is travesed, for late checking untraced grammars
                traced.push(grammar);
                //if epsilon is in the sub grammar.first() means this grammar will look the next symbol.first()
                const nextFirst = grammar.first();
                const hasEpsilon = nextFirst.filter(f=>f===Terminals.EPSILON).length>0;
                nextFirst.filter(t=>t!==Terminals.EPSILON).forEach(t=>production.first().push(t));
                //if non terminal doesn't have epsilon, the traves can break
                if(!hasEpsilon){
                    broken = true;
                    break;
                }
            }
            // if all symbols in the production can derive epsilon, add epsilon
            if(!broken){
                production.first().push(Terminals.EPSILON);
            }
            if(production.first().length>oldSize){
                changed.set(true);
            }
        });
        grammars.filter(g=>traced.includes(g)).forEach(g=>Tool.warn(`grammar ${g.name()} is not traced.`));
    }

    function computeFollow(){
        compute((production,changed)=>{
            const grammar = production.grammar();
            const symbols = trimProductionSymbols(production.symbols());
            const first = [];//the first of next non terminal is my follow
            let containsEmpty = true;
            //travese from right to left
            symbols.reverse().forEach(symbol=>{
                 //if there is a production rule X → ...Aa..., where A is a non-terminal and a is a terminal, then add a to the follow set of A.
                 //if this one is terminal, then the next symbol must be non terminal (Aa)
                if(symbol.isTerminal()){
                    first = [];
                    first.push(symbol);
                    containsEmpty = false;
                    return;
                }
                //if g is A (non terminal)
                const g = grammars[symbol.name()];
                const oldSize = g.follow().size();
                //if there is a production rule X → ...AB, contiansEmpty means there is a B next A, and there is no a next B =>X.follow must be in B.follow 
                if(containsEmpty===true){
                    grammar.follow().forEach(f=>g.follow().push(f));
                }
               
                //X → ...AB => B.first must be in A.follow
                first.forEach(f=>g.follow().push(f));
                //X → ...ABC, when B derives epsilon  => C.first must be in A.follow
                if(g.first().filter(t=>t===Terminals.EPSILON).length===0){
                    first.clear();
                    containsEmpty = false;
                }
                first = first.concat(g.first());
                if(g.follow().length>oldSize){
                    changed.set(true);
                }
            })
        });
    }

    /**
     * remove left side terminals of first non terminal, leave only one terminal right next the last non terminal
     * trim(abcDef) = De
     */
    function trimProductionSymbols(origin){
        const trimed = [...origin];
        while(trimed[0].isTerminal()===true){
            trimed.shift();
        }
        while(trimed.length>2 && trimed(trimed.length-1).isTerminal()===true && trimed(trimed.length-2).isTerminal()===false){
            trimed.pop();
        }
        return trimed;
    }

    return {
        /**
         * add grammar from grammar string
         */
        addGrammar: grammarStr=>{
            const [g,ps] = grammarStr.split(CONSTANTS.MINUS_ARROW);
            //new grammar
            const grammar = createGrammar(g);
            //new productions under the new grammar
            ps.split(CONSTANTS.PIPE).forEach(p=>grammar.addProduction(p))
            return grammar;
        },
        /**
         * verify all grammars
         * prepare first and follow
         */
        build:()=>{
            verifyStart();
            computeFirst();
            computeFollow();
        }

    };
})();

export {Grammars};